/*
 * Copyright (c) Kumo Inc. and affiliates.
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <melon/json/json_schema.h>
#include <melon/json/json.h>
#include <melon/portability/gtest.h>

using melon::Dynamic;
using melon::parseJson;
using namespace melon::jsonschema;
using namespace std;

bool check(const Dynamic &schema, const Dynamic &value, bool check = true) {
    if (check) {
        auto schemavalidator = makeSchemaValidator();
        auto ew = schemavalidator->try_validate(schema);
        if (ew) {
            return false;
        }
    }

    auto validator = makeValidator(schema);
    auto ew = validator->try_validate(value);
    if (validator->try_validate(value)) {
        return false;
    }
    return true;
}

TEST(JSONSchemaTest, TestMultipleOfInt) {
    Dynamic schema = Dynamic::object("multipleOf", 2);
    ASSERT_TRUE(check(schema, "invalid"));
    ASSERT_TRUE(check(schema, 30));
    ASSERT_TRUE(check(schema, 24.0));
    ASSERT_FALSE(check(schema, 5));
    ASSERT_FALSE(check(schema, 2.01));
}

TEST(JSONSchemaTest, TestMultipleOfDouble) {
    Dynamic schema = Dynamic::object("multipleOf", 1.5);
    ASSERT_TRUE(check(schema, "invalid"));
    ASSERT_TRUE(check(schema, 30));
    ASSERT_TRUE(check(schema, 24.0));
    ASSERT_FALSE(check(schema, 5));
    ASSERT_FALSE(check(schema, 2.01));

    schema = Dynamic::object("multipleOf", 0.0001);
    ASSERT_TRUE(check(schema, 0.0075));
}

TEST(JSONSchemaTest, TestMinimumIntInclusive) {
    Dynamic schema = Dynamic::object("minimum", 2);
    ASSERT_TRUE(check(schema, "invalid"));
    ASSERT_TRUE(check(schema, 30));
    ASSERT_TRUE(check(schema, 24.0));
    ASSERT_TRUE(check(schema, 2));
    ASSERT_FALSE(check(schema, 1));
    ASSERT_FALSE(check(schema, 1.9999));
}

TEST(JSONSchemaTest, TestMinimumIntExclusive) {
    Dynamic schema = Dynamic::object("minimum", 2)("exclusiveMinimum", true);
    ASSERT_FALSE(check(schema, 2));
}

TEST(JSONSchemaTest, TestMaximumIntInclusive) {
    Dynamic schema = Dynamic::object("maximum", 12);
    ASSERT_TRUE(check(schema, "invalid"));
    ASSERT_TRUE(check(schema, 3));
    ASSERT_TRUE(check(schema, 3.1));
    ASSERT_TRUE(check(schema, 12));
    ASSERT_FALSE(check(schema, 13));
    ASSERT_FALSE(check(schema, 12.0001));
}

TEST(JSONSchemaTest, TestMaximumIntExclusive) {
    Dynamic schema = Dynamic::object("maximum", 2)("exclusiveMaximum", true);
    ASSERT_FALSE(check(schema, 2));
}

TEST(JSONSchemaTest, TestMinimumDoubleInclusive) {
    Dynamic schema = Dynamic::object("minimum", 1.75);
    ASSERT_TRUE(check(schema, "invalid"));
    ASSERT_TRUE(check(schema, 30));
    ASSERT_TRUE(check(schema, 24.0));
    ASSERT_TRUE(check(schema, 1.75));
    ASSERT_FALSE(check(schema, 1));
    ASSERT_FALSE(check(schema, 1.74));
}

TEST(JSONSchemaTest, TestMinimumDoubleExclusive) {
    Dynamic schema = Dynamic::object("minimum", 1.75)("exclusiveMinimum", true);
    ASSERT_FALSE(check(schema, 1.75));
}

TEST(JSONSchemaTest, TestMaximumDoubleInclusive) {
    Dynamic schema = Dynamic::object("maximum", 12.75);
    ASSERT_TRUE(check(schema, "invalid"));
    ASSERT_TRUE(check(schema, 3));
    ASSERT_TRUE(check(schema, 3.1));
    ASSERT_TRUE(check(schema, 12.75));
    ASSERT_FALSE(check(schema, 13));
    ASSERT_FALSE(check(schema, 12.76));
}

TEST(JSONSchemaTest, TestMaximumDoubleExclusive) {
    Dynamic schema = Dynamic::object("maximum", 12.75)("exclusiveMaximum", true);
    ASSERT_FALSE(check(schema, 12.75));
}

TEST(JSONSchemaTest, TestInvalidSchema) {
    Dynamic schema = Dynamic::object("multipleOf", "invalid");
    // don't check the schema since it's meant to be invalid
    ASSERT_TRUE(check(schema, 30, false));

    schema = Dynamic::object("minimum", "invalid")("maximum", "invalid");
    ASSERT_TRUE(check(schema, 2, false));

    schema = Dynamic::object("minLength", "invalid")("maxLength", "invalid");
    ASSERT_TRUE(check(schema, 2, false));
    ASSERT_TRUE(check(schema, "foo", false));
}

TEST(JSONSchemaTest, TestMinimumStringLength) {
    Dynamic schema = Dynamic::object("minLength", 3);
    ASSERT_TRUE(check(schema, "abcde"));
    ASSERT_TRUE(check(schema, "abc"));
    ASSERT_FALSE(check(schema, "a"));
}

TEST(JSONSchemaTest, TestMaximumStringLength) {
    Dynamic schema = Dynamic::object("maxLength", 3);
    ASSERT_FALSE(check(schema, "abcde"));
    ASSERT_TRUE(check(schema, "abc"));
    ASSERT_TRUE(check(schema, "a"));
}

TEST(JSONSchemaTest, TestStringPattern) {
    Dynamic schema = Dynamic::object("pattern", "[1-9]+");
    ASSERT_TRUE(check(schema, "123"));
    ASSERT_FALSE(check(schema, "abc"));
}

TEST(JSONSchemaTest, TestMinimumArrayItems) {
    Dynamic schema = Dynamic::object("minItems", 3);
    ASSERT_TRUE(check(schema, Dynamic::array(1, 2, 3, 4, 5)));
    ASSERT_TRUE(check(schema, Dynamic::array(1, 2, 3)));
    ASSERT_FALSE(check(schema, Dynamic::array(1)));
}

TEST(JSONSchemaTest, TestMaximumArrayItems) {
    Dynamic schema = Dynamic::object("maxItems", 3);
    ASSERT_FALSE(check(schema, Dynamic::array(1, 2, 3, 4, 5)));
    ASSERT_TRUE(check(schema, Dynamic::array(1, 2, 3)));
    ASSERT_TRUE(check(schema, Dynamic::array(1)));
    ASSERT_TRUE(check(schema, "foobar"));
}

TEST(JSONSchemaTest, TestArrayUniqueItems) {
    Dynamic schema = Dynamic::object("uniqueItems", true);
    ASSERT_TRUE(check(schema, Dynamic::array(1, 2, 3)));
    ASSERT_FALSE(check(schema, Dynamic::array(1, 2, 3, 1)));
    ASSERT_FALSE(check(schema, Dynamic::array("cat", "dog", 1, 2, "cat")));
    ASSERT_TRUE(check(
        schema,
        Dynamic::array(
            Dynamic::object("foo", "bar"), Dynamic::object("foo", "baz"))));

    schema = Dynamic::object("uniqueItems", false);
    ASSERT_TRUE(check(schema, Dynamic::array(1, 2, 3, 1)));
}

TEST(JSONSchemaTest, TestArrayItems) {
    Dynamic schema = Dynamic::object("items", Dynamic::object("minimum", 2));
    ASSERT_TRUE(check(schema, Dynamic::array(2, 3, 4)));
    ASSERT_FALSE(check(schema, Dynamic::array(3, 4, 1)));
}

TEST(JSONSchemaTest, TestArrayAdditionalItems) {
    Dynamic schema = Dynamic::object(
        "items",
        Dynamic::array(
            Dynamic::object("minimum", 2), Dynamic::object("minimum", 1)))(
        "additionalItems", Dynamic::object("minimum", 3));
    ASSERT_TRUE(check(schema, Dynamic::array(2, 1, 3, 3, 3, 3, 4)));
    ASSERT_FALSE(check(schema, Dynamic::array(2, 1, 3, 3, 3, 3, 1)));
}

TEST(JSONSchemaTest, TestArrayNoAdditionalItems) {
    Dynamic schema =
            Dynamic::object("items", Dynamic::array(Dynamic::object("minimum", 2)))(
                "additionalItems", false);
    ASSERT_FALSE(check(schema, Dynamic::array(3, 3, 3)));
}

TEST(JSONSchemaTest, TestArrayItemsNotPresent) {
    Dynamic schema = Dynamic::object("additionalItems", false);
    ASSERT_TRUE(check(schema, Dynamic::array(3, 3, 3)));
}

TEST(JSONSchemaTest, TestRef) {
    Dynamic schema = Dynamic::object(
        "definitions",
        Dynamic::object(
            "positiveInteger", Dynamic::object("minimum", 1)("type", "integer")))(
        "items", Dynamic::object("$ref", "#/definitions/positiveInteger"));
    ASSERT_TRUE(check(schema, Dynamic::array(1, 2, 3, 4)));
    ASSERT_FALSE(check(schema, Dynamic::array(4, -5)));
}

TEST(JSONSchemaTest, TestRecursiveRef) {
    Dynamic schema = Dynamic::object(
        "properties", Dynamic::object("more", Dynamic::object("$ref", "#")));
    Dynamic d = Dynamic::object;
    ASSERT_TRUE(check(schema, d));
    d["more"] = Dynamic::object;
    ASSERT_TRUE(check(schema, d));
    d["more"]["more"] = Dynamic::object;
    ASSERT_TRUE(check(schema, d));
    d["more"]["more"]["more"] = Dynamic::object;
    ASSERT_TRUE(check(schema, d));
}

TEST(JSONSchemaTest, TestDoubleRecursiveRef) {
    Dynamic schema = Dynamic::object(
        "properties",
        Dynamic::object("more", Dynamic::object("$ref", "#"))(
            "less", Dynamic::object("$ref", "#")));
    Dynamic d = Dynamic::object;
    ASSERT_TRUE(check(schema, d));
    d["more"] = Dynamic::object;
    d["less"] = Dynamic::object;
    ASSERT_TRUE(check(schema, d));
    d["more"]["less"] = Dynamic::object;
    d["less"]["mode"] = Dynamic::object;
    ASSERT_TRUE(check(schema, d));
}

TEST(JSONSchemaTest, TestInfinitelyRecursiveRef) {
    Dynamic schema = Dynamic::object("not", Dynamic::object("$ref", "#"));
    auto validator = makeValidator(schema);
    ASSERT_THROW(validator->validate(Dynamic::array(1, 2)), std::runtime_error);
}

TEST(JSONSchemaTest, TestRequired) {
    Dynamic schema = Dynamic::object("required", Dynamic::array("foo", "bar"));
    ASSERT_FALSE(check(schema, Dynamic::object("foo", 123)));
    ASSERT_FALSE(check(schema, Dynamic::object("bar", 123)));
    ASSERT_TRUE(check(schema, Dynamic::object("bar", 123)("foo", 456)));
}

TEST(JSONSchemaTest, TestMinMaxProperties) {
    Dynamic schema = Dynamic::object("minProperties", 1)("maxProperties", 3);
    Dynamic d = Dynamic::object;
    ASSERT_FALSE(check(schema, d));
    d["a"] = 1;
    ASSERT_TRUE(check(schema, d));
    d["b"] = 2;
    ASSERT_TRUE(check(schema, d));
    d["c"] = 3;
    ASSERT_TRUE(check(schema, d));
    d["d"] = 4;
    ASSERT_FALSE(check(schema, d));
}

TEST(JSONSchemaTest, TestProperties) {
    Dynamic schema = Dynamic::object(
        "properties", Dynamic::object("p1", Dynamic::object("minimum", 1)))(
        "patternProperties", Dynamic::object("[0-9]+", Dynamic::object))(
        "additionalProperties", Dynamic::object("maximum", 5));
    ASSERT_TRUE(check(schema, Dynamic::object("p1", 1)));
    ASSERT_FALSE(check(schema, Dynamic::object("p1", 0)));
    ASSERT_TRUE(check(schema, Dynamic::object("123", "anything")));
    ASSERT_TRUE(check(schema, Dynamic::object("123", 500)));
    ASSERT_TRUE(check(schema, Dynamic::object("other_property", 4)));
    ASSERT_FALSE(check(schema, Dynamic::object("other_property", 6)));
}

TEST(JSONSchemaTest, TestPropertyAndPattern) {
    Dynamic schema = Dynamic::object(
        "properties", Dynamic::object("p1", Dynamic::object("minimum", 1)))(
        "patternProperties",
        Dynamic::object("p.", Dynamic::object("maximum", 5)));
    ASSERT_TRUE(check(schema, Dynamic::object("p1", 3)));
    ASSERT_FALSE(check(schema, Dynamic::object("p1", 0)));
    ASSERT_FALSE(check(schema, Dynamic::object("p1", 6)));
}

TEST(JSONSchemaTest, TestPropertyDependency) {
    Dynamic schema = Dynamic::object(
        "dependencies", Dynamic::object("p1", Dynamic::array("p2")));
    ASSERT_TRUE(check(schema, Dynamic::object));
    ASSERT_TRUE(check(schema, Dynamic::object("p1", 1)("p2", 1)));
    ASSERT_FALSE(check(schema, Dynamic::object("p1", 1)));
}

TEST(JSONSchemaTest, TestSchemaDependency) {
    Dynamic schema = Dynamic::object(
        "dependencies",
        Dynamic::object("p1", Dynamic::object("required", Dynamic::array("p2"))));
    ASSERT_TRUE(check(schema, Dynamic::object));
    ASSERT_TRUE(check(schema, Dynamic::object("p1", 1)("p2", 1)));
    ASSERT_FALSE(check(schema, Dynamic::object("p1", 1)));
}

TEST(JSONSchemaTest, TestEnum) {
    Dynamic schema = Dynamic::object("enum", Dynamic::array("a", 1));
    ASSERT_TRUE(check(schema, "a"));
    ASSERT_TRUE(check(schema, 1));
    ASSERT_FALSE(check(schema, "b"));
}

TEST(JSONSchemaTest, TestType) {
    Dynamic schema = Dynamic::object("type", "object");
    ASSERT_TRUE(check(schema, Dynamic::object));
    ASSERT_FALSE(check(schema, Dynamic(5)));
}

TEST(JSONSchemaTest, TestTypeArray) {
    Dynamic schema = Dynamic::object("type", Dynamic::array("array", "number"));
    ASSERT_TRUE(check(schema, Dynamic(5)));
    ASSERT_TRUE(check(schema, Dynamic(1.1)));
    ASSERT_FALSE(check(schema, Dynamic::object));
}

TEST(JSONSchemaTest, TestAllOf) {
    Dynamic schema = Dynamic::object(
        "allOf",
        Dynamic::array(
            Dynamic::object("minimum", 1), Dynamic::object("type", "integer")));
    ASSERT_TRUE(check(schema, 2));
    ASSERT_FALSE(check(schema, 0));
    ASSERT_FALSE(check(schema, 1.1));
}

TEST(JSONSchemaTest, TestAnyOf) {
    Dynamic schema = Dynamic::object(
        "anyOf",
        Dynamic::array(
            Dynamic::object("minimum", 1), Dynamic::object("type", "integer")));
    ASSERT_TRUE(check(schema, 2)); // matches both
    ASSERT_FALSE(check(schema, 0.1)); // matches neither
    ASSERT_TRUE(check(schema, 1.1)); // matches first one
    ASSERT_TRUE(check(schema, 0)); // matches second one
}

TEST(JSONSchemaTest, TestOneOf) {
    Dynamic schema = Dynamic::object(
        "oneOf",
        Dynamic::array(
            Dynamic::object("minimum", 1), Dynamic::object("type", "integer")));
    ASSERT_FALSE(check(schema, 2)); // matches both
    ASSERT_FALSE(check(schema, 0.1)); // matches neither
    ASSERT_TRUE(check(schema, 1.1)); // matches first one
    ASSERT_TRUE(check(schema, 0)); // matches second one
}

TEST(JSONSchemaTest, TestNot) {
    Dynamic schema =
            Dynamic::object("not", Dynamic::object("minimum", 5)("maximum", 10));
    ASSERT_TRUE(check(schema, 4));
    ASSERT_FALSE(check(schema, 7));
    ASSERT_TRUE(check(schema, 11));
}

// The tests below use some sample schema from json-schema.org

TEST(JSONSchemaTest, TestMetaSchema) {
    const char *example1 =
            "\
    { \
      \"title\": \"Example Schema\", \
      \"type\": \"object\", \
      \"properties\": { \
        \"firstName\": { \
          \"type\": \"string\" \
        }, \
        \"lastName\": { \
          \"type\": \"string\" \
        }, \
        \"age\": { \
          \"description\": \"Age in years\", \
          \"type\": \"integer\", \
          \"minimum\": 0 \
        } \
      }, \
      \"required\": [\"firstName\", \"lastName\"] \
    }";

    auto val = makeSchemaValidator();
    val->validate(parseJson(example1)); // doesn't throw

    ASSERT_THROW(val->validate("123"), std::runtime_error);
}

TEST(JSONSchemaTest, TestProductSchema) {
    const char *productSchema =
            "\
  { \
    \"$schema\": \"http://json-schema.org/draft-04/schema#\", \
      \"title\": \"Product\", \
      \"description\": \"A product from Acme's catalog\", \
      \"type\": \"object\", \
      \"properties\": { \
        \"id\": { \
          \"description\": \"The unique identifier for a product\", \
          \"type\": \"integer\" \
        }, \
        \"name\": { \
          \"description\": \"Name of the product\", \
          \"type\": \"string\" \
        }, \
        \"price\": { \
          \"type\": \"number\", \
          \"minimum\": 0, \
          \"exclusiveMinimum\": true \
        }, \
        \"tags\": { \
          \"type\": \"array\", \
          \"items\": { \
            \"type\": \"string\" \
          }, \
          \"minItems\": 1, \
          \"uniqueItems\": true \
        } \
      }, \
      \"required\": [\"id\", \"name\", \"price\"] \
  }";
    const char *product =
            "\
  { \
    \"id\": 1, \
    \"name\": \"A green door\", \
    \"price\": 12.50, \
    \"tags\": [\"home\", \"green\"] \
  }";
    ASSERT_TRUE(check(parseJson(productSchema), parseJson(product)));
}
